#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#


import time
import socket
import random
import threading
import traceback


from proxy import Proxy
from help import logging

from id import ID
from url import URL


from help.bencode import bencode as encode
from help.bencode import bdecode as decode

class   TcpProxy(Proxy) :
    rbufsize = -1
    wbufsize = 0

    class   CacheItem(object) :
        def __init__(self, data=None, timeout=0) :
            self.data = data
            self.timeout = timeout
            ## __init__()

        ## class CacheItem

    class   Cache(object) :
        def __init__(self, timeout=0) :
            self._boxes = {}
            self._timeout = 0
            ## __init__()

        def __del__(self) :
            self._box = {}
            ## __del__()

        def put(self, category, key, data, timeout=0) :
            try :
                box = self._boxes[ category ]
            except :
                box = {}
                self._boxes[ category ] = box
            item = TcpProxy.CacheItem(data, timeout)
            box[ key ] = item
            ## put()

        def get(self, category, key) :
            try :
                box = self._boxes[ category ]
            except :
                return None
            try :
                item = box[ key ]
                return item.data
            except :
                return None
            ## get()

        ## class Cache

    # class variable
    _cache = Cache()


    def __init__(self, url) :
        super(TcpProxy, self).__init__(url)

        self.s = None
        self.wfile = None
        self.rfile = None

        self.udp_channel = None

        self._cache_category = self.url.getURL()

        self.logger = logging.getLogger('OpenChord')
        ## __init__()

    def __del__(self) :
        self.closeUdpChannel()
        ## __del__()

    def open(self) :
        pass
        ## open()

    def close(self) :
        pass
        ## close()

    def ping(self) :
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try :
            try :
                s.connect((self.url.getHost(), self.url.getPort()))
            except :
                return False
        finally :
            s.close()

        return True
        ## ping()

    def connect(self) :
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((self.url.getHost(), self.url.getPort()))

        self.s = s
        self.wfile = self.s.makefile('wb', self.wbufsize)
        self.rfile = self.s.makefile('rb', self.rbufsize)

        ## connect()

    def disconnect(self) :
        if self.wfile :
            self.wfile.close()
            self.wfile = None
        if self.rfile :
            self.rfile.close()
            self.rifle = None
        if self.s :
            self.s.close()
            self.s = None
        ## disconnect()

    def pre_callout(self, method, parameters) :
        method_encode = str(method)
        params_encode = encode(parameters)

        return method_encode,params_encode
        ## pre_callout()

    def callout(self, method, parameters) :
        try :
            try :
                self.connect()

                meth,params = self.pre_callout(method, parameters)
                self.wfile.write('\t'.join((str(meth),str(params),'\n')))
                self.wfile.flush()
                response = None
                while not response :
                    response = self.rfile.readline().strip()
                #self.post_callout(response)
            except :
                exinfo = traceback.format_exc()
                self.logger.error('CALLOUT Exception : %s', str(exinfo))
                return None
        finally :
            self.disconnect()
        result = self.post_callout(response.strip())

        return result
        ## callout()

    def post_callout(self, response) :
        return decode(response)
        ## post_callout()

    def udp_callout(self, method, parameters) :
        self.openUdpChannel()

        try :
            if self.logger.isTraceEnabled() : self.logger.trace('CallOut/UDP : method=%s parameters=%s', method, parameters)
            meth,params = self.pre_callout(method, parameters)
            pin_sent = ''.join((self.udp_pin_prefix, str(self.__getUdpPinSerial())))
            self.udp_channel.sendto('\t'.join((pin_sent,str(meth),str(params),'\n')), self.udp_address)
            while True :
                received = self.udp_channel.recv(65536)
                pin_recv, response = received.split('\t', 1)
                if pin_sent == pin_recv : break
            result = self.post_callout(response.strip())
            if self.logger.isTraceEnabled() : self.logger.trace('CallOut/UDP : method=%s parameters=%s response=%s', method, parameters, result)
        except :
            self.logger.excpt()
            return None

        return result
        ## udp_callout()

    def openUdpChannel(self) :
        if not self.udp_channel :
            self.udp_channel = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.udp_address = (self.url.getHost(), self.url.getPort())
            self.udp_channel.settimeout(60)
            random.seed(time.time())
            self.udp_pin_prefix = 'UDP%X.' % random.randint(65536,1048575)
            self.udp_pin_sn_MIN = 1000
            self.udp_pin_sn_MAX = 100000000
            self.udp_pin_sn = self.udp_pin_sn_MIN
            self.udp_pin_sn_lock = threading.Lock()
        ## openUdpChannel()

    def closeUdpChannel(self) :
        if self.udp_channel :
            self.udp_channel.close()
            self.udp_channel = None
        ## closeUdpChannel()

    def __getUdpPinSerial(self) :
        self.udp_pin_sn_lock.acquire()
        self.udp_pin_sn += 1
        if self.udp_pin_sn >= self.udp_pin_sn_MAX :
            self.udp_pin_sn = self.udp_pin_sn_MIN + 1
        self.udp_pin_sn_lock.release()
        return self.udp_pin_sn
        ## __getUdpPinSerial()

    #### --------------------------------------------------------- ####


    def findSuccessor(self, id) :
        method = 'findSuccessor'

        id_n = 0
        if isinstance(id, int) :
            id_n = id
        elif isinstance(id, ID) :
            id_n = id.value
        else : 
            raise TypeError, 'Invalid id type "%s"' % type(id)

        parameters = id_n
        result = self.udp_callout(method, parameters)

        # to check that result is URL of remote node
        url_successor = result

        proxy = TcpProxy( URL(url_successor) )
        return proxy
        ## findSuccessor()


    def getSuccessor(self) :
        method = 'getSuccessor'

        parameters = 'N/A'
        result = self.udp_callout(method, parameters)

        # to check that result is URL of remote node
        url_successor = result

        proxy = TcpProxy( URL(url_successor) )
        return proxy
        ## getSuccessor()

    def getPredecessor(self) :
        method = 'getPredecessor'

        parameters = 'N/A'
        result = self.udp_callout(method, parameters)

        # to check that result is URL of remote node
        url_predecessor = result

        proxy = TcpProxy( URL(url_predecessor) )
        return proxy
        ## getPredecessor()

    def setPredecessor(self, predecessor) :
        method = 'setPredecessor'

        if isinstance(predecessor, Proxy) :
            url_predecessor = predecessor.url.getURL()
        elif isinstance(predecessor, str) :
            url_predecessor = predecessor
        else :
            raise TypeError, 'Invalid predecessor type "%s"' % type(predecessor)

        parameters = url_predecessor
        result = self.udp_callout(method, parameters)
        
        # to check that result 
        self.logger.debug('TODO check result of method %s', method)

        return True
        ## setPredecessor()

    def getID(self) :
        method = 'getID'

        cached = self._cache.get( self._cache_category, method )
        #self.logger.debug('CACHE-get: category:%s key:%s data=%s', self._cache_category, method, str(cached))
        if cached :
            return cached

        parameters = 'N/A'
        result = self.udp_callout(method, parameters)

        # to check that result is id of remote node
        id = result

        cached = ID(id)
        self._cache.put( self._cache_category, method, cached )
        #self.logger.debug('CACHE-put: category:%s key:%s data=%s', self._cache_category, method, str(cached))
        return cached
        ## getID()

    def getInterval(self) :
        method = 'getInterval'

        cached = self._cache.get( self._cache_category, method )
        #self.logger.debug('CACHE-get: category:%s key:%s data=%s', self._cache_category, method, str(cached))
        if cached :
            return cached

        parameters = 'N/A'
        result = self.udp_callout(method, parameters)

        # to check that result is id of remote node
        p_id_n, l_id_n = result

        interval = (ID(p_id_n), ID(l_id_n))
        cached = interval
        self._cache.put( self._cache_category, method, cached )
        #self.logger.debug('CACHE-put: category:%s key:%s data=%s', self._cache_category, method, str(cached))
        return cached
        ## getInterval()

    def getReplicas(self) :
        method = 'getReplicas'

        cached = self._cache.get( self._cache_category, method )
        #self.logger.debug('CACHE-get: category:%s key:%s data=%s', self._cache_category, method, str(cached))
        if cached :
            return cached

        parameters = 'N/A'
        result = self.udp_callout(method, parameters)

        # to check that result is URL of remote node
        url_replicas = result
        replicas = []
        for url in url_replicas :
            proxy = TcpProxy( URL(url) )
            replicas.append(proxy)

        cached = replicas
        self._cache.put( self._cache_category, method, cached )
        #self.logger.debug('CACHE-put: category:%s key:%s data=%s', self._cache_category, method, str(cached))
        return cached
        ## getReplicas()

    def findClosestPrecedingFinger(self, id) :
        method = 'findClosestPrecedingFinger'

        id_n = 0
        if isinstance(id, int) :
            id_n = id
        elif isinstance(id, ID) :
            id_n = id.value
        else : 
            raise TypeError, 'Invalid id type "%s"' % type(id)

        parameters = id_n
        result = self.udp_callout(method, parameters)

        # to check that result is URL of remote node
        url_successor = result

        proxy = TcpProxy( URL(url_successor) )
        return proxy
        ## findClosestPrecedingFinger()

    def updateFingerTable(self, node, index) :
        method = 'updateFingerTable'

        url = node.url.getURL()
        self.logger.trace('CALLOUT update N#%s\'s %d-th finger to %s', self.url.getURL(), index, url)

        parameters = (url, index)
        result = self.udp_callout(method, parameters)

        # to check that result 
        #self.logger.debug('TODO check result of method %s', method)

        return True
        ## updateFingerTable

    def claimPredecessor(self, predecessor) :
        method = 'claimPredecessor'

        if isinstance(predecessor, Proxy) :
            url_predecessor = predecessor.url.getURL()
        elif isinstance(predecessor, str) :
            url_predecessor = predecessor
        else :
            raise TypeError, 'Invalid predecessor type "%s"' % type(predecessor)

        parameters = url_predecessor
        result = self.udp_callout(method, parameters)
        
        # to check that result 
        ##self.logger.debug('TODO check result of method %s', method)

        return True
        ## claimPredecessor()

    #### --------------------------------------------------------- ####

    def insert(self, key_id, key, data, timestamp) :
        method = 'insert'

        parameters = (key_id, key, data, timestamp)
        result = self.callout(method, parameters)
        self.logger.debug('CALLOUT method:%s result:%s', method, result)

        # to check that result 
        op_status,op_key_id,op_key = result

        return (op_status, op_key_id, op_key)
        ## insert()

    def retrieve(self, key_id, key, timestamp) :
        method = 'retrieve'

        parameters = (key_id, key, timestamp)
        result = self.callout(method, parameters)

        # to check that result 
        op_status,op_key_id,op_key,op_data,op_timestamp = result

        return (op_status, op_key_id, op_key, op_data, op_timestamp)
        ## retrieve()

    def remove(self, key_id, key, timestamp) :
        method = 'remove'

        parameters = (key_id, key, timestamp)
        result = self.callout(method, parameters)

        # to check that result 
        op_status,op_key_id,op_key = result

        return (op_status, op_key_id, op_key)
        ## remove()


    #### --------------------------------------------------------- ####


    ## class TcpProxy


